/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.url;

import com.google.common.base.Function;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import javax.xml.xpath.XPathExpression;
import org.apache.commons.lang.StringUtils;
import org.apereo.portal.IUserPreferencesManager;
import org.apereo.portal.PortalException;
import org.apereo.portal.concurrency.caching.RequestCache;
import org.apereo.portal.dao.usertype.FunctionalNameType;
import org.apereo.portal.layout.IStylesheetUserPreferencesService;
import org.apereo.portal.layout.IStylesheetUserPreferencesService.PreferencesScope;
import org.apereo.portal.layout.IUserLayout;
import org.apereo.portal.layout.IUserLayoutManager;
import org.apereo.portal.layout.PortletTabIdResolver;
import org.apereo.portal.layout.node.IUserLayoutNodeDescription;
import org.apereo.portal.layout.om.IStylesheetDescriptor;
import org.apereo.portal.layout.om.IStylesheetParameterDescriptor;
import org.apereo.portal.portlet.om.IPortletDefinition;
import org.apereo.portal.portlet.om.IPortletEntity;
import org.apereo.portal.portlet.om.IPortletEntityId;
import org.apereo.portal.portlet.om.IPortletWindow;
import org.apereo.portal.portlet.om.IPortletWindowId;
import org.apereo.portal.portlet.registry.IPortletEntityRegistry;
import org.apereo.portal.portlet.registry.IPortletWindowRegistry;
import org.apereo.portal.user.IUserInstance;
import org.apereo.portal.user.IUserInstanceManager;
import org.apereo.portal.xml.xpath.XPathOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * Maps tabs and portlets to folder names and back. Handles a single set of tabs and uses tab IDs
 * for folder names.
 */
@Service
public class SingleTabUrlNodeSyntaxHelper implements IUrlNodeSyntaxHelper {
    public static final String EXTERNAL_ID_ATTR = "externalId";

    private static final char PORTLET_PATH_ELEMENT_SEPERATOR = '.';

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private String defaultLayoutNodeIdExpression =
            "/layout/folder/folder[@type='regular' and @hidden!='true'][$defaultTab]/@ID";
    //    private String tabIdExpression = "/layout/folder/folder[@ID=$nodeId or
    // descendant::node()[@ID=$nodeId]]/@ID";
    private String defaultTabParameter = "defaultTab";

    private IUserInstanceManager userInstanceManager;
    private XPathOperations xpathOperations;
    private IStylesheetUserPreferencesService stylesheetUserPreferencesService;
    private IPortletWindowRegistry portletWindowRegistry;
    private IPortletEntityRegistry portletEntityRegistry;

    @Autowired
    public void setPortletWindowRegistry(IPortletWindowRegistry portletWindowRegistry) {
        this.portletWindowRegistry = portletWindowRegistry;
    }

    @Autowired
    public void setPortletEntityRegistry(IPortletEntityRegistry portletEntityRegistry) {
        this.portletEntityRegistry = portletEntityRegistry;
    }

    @Autowired
    public void setUserInstanceManager(IUserInstanceManager userInstanceManager) {
        this.userInstanceManager = userInstanceManager;
    }

    @Autowired
    public void setXpathOperations(XPathOperations xpathOperations) {
        this.xpathOperations = xpathOperations;
    }

    @Autowired
    public void setStylesheetUserPreferencesService(
            IStylesheetUserPreferencesService stylesheetUserPreferencesService) {
        this.stylesheetUserPreferencesService = stylesheetUserPreferencesService;
    }

    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public String getDefaultLayoutNodeId(HttpServletRequest httpServletRequest) {
        final IUserInstance userInstance =
                this.userInstanceManager.getUserInstance(httpServletRequest);
        final IUserPreferencesManager preferencesManager = userInstance.getPreferencesManager();
        final IUserLayoutManager userLayoutManager = preferencesManager.getUserLayoutManager();

        final IUserLayout userLayout = userLayoutManager.getUserLayout();

        // This logic is specific to tab/column layouts
        final String defaultTabIndex = this.getDefaultTabIndex(httpServletRequest);
        if (defaultTabIndex != null) {
            final String defaultTabId = this.getTabId(userLayout, defaultTabIndex);
            if (StringUtils.isNotEmpty(defaultTabId)) {
                return defaultTabId;
            }
        }

        this.logger.warn(
                "Failed to find default tab id for '"
                        + userInstance.getPerson().getUserName()
                        + "' with default tab index "
                        + defaultTabIndex
                        + ". Index 1 will be tried as a fall-back.");

        final String firstTabId = getTabId(userLayout, "1");
        if (StringUtils.isNotEmpty(firstTabId)) {
            return firstTabId;
        }

        this.logger.warn(
                "Failed to find default tab id for '"
                        + userInstance.getPerson().getUserName()
                        + "' with default tab index 1. The user has no tabs.");

        return userLayout.getRootId();
    }

    protected String getTabId(final IUserLayout userLayout, final String tabIndex) {
        return this.xpathOperations.doWithExpression(
                defaultLayoutNodeIdExpression,
                Collections.singletonMap("defaultTab", tabIndex),
                new Function<XPathExpression, String>() {
                    @Override
                    public String apply(XPathExpression xPathExpression) {
                        return userLayout.findNodeId(xPathExpression);
                    }
                });
    }

    /** Get the index of the default tab for the user */
    protected String getDefaultTabIndex(HttpServletRequest httpServletRequest) {
        final String stylesheetParameter =
                this.stylesheetUserPreferencesService.getStylesheetParameter(
                        httpServletRequest, PreferencesScope.STRUCTURE, this.defaultTabParameter);
        if (stylesheetParameter != null) {
            return stylesheetParameter;
        }

        final IStylesheetDescriptor stylesheetDescriptor =
                this.stylesheetUserPreferencesService.getStylesheetDescriptor(
                        httpServletRequest, PreferencesScope.STRUCTURE);
        final IStylesheetParameterDescriptor stylesheetParameterDescriptor =
                stylesheetDescriptor.getStylesheetParameterDescriptor(this.defaultTabParameter);
        if (stylesheetParameterDescriptor != null) {
            return stylesheetParameterDescriptor.getDefaultValue();
        }

        return null;
    }

    @RequestCache(keyMask = {false, true})
    @Override
    public List<String> getFolderNamesForLayoutNode(
            HttpServletRequest request, String layoutNodeId) {
        /*
         * Implementation note:
         * While the API allows one or more folder names, this implementation will only ever return
         * a List with zero or one element.  It's not entirely clear that a List with zero members
         * was allowed by the interface definition, but this implementation will return an empty
         * list if the layoutNodeId cannot be found in the layout.
         */

        final IUserInstance userInstance = this.userInstanceManager.getUserInstance(request);
        final IUserPreferencesManager preferencesManager = userInstance.getPreferencesManager();
        final IUserLayoutManager userLayoutManager = preferencesManager.getUserLayoutManager();
        final IUserLayout userLayout = userLayoutManager.getUserLayout();

        final String tabId = userLayout.findNodeId(new PortletTabIdResolver(layoutNodeId));

        if (StringUtils.isEmpty(tabId)) {
            return Collections.emptyList();
        }

        String externalId =
                stylesheetUserPreferencesService.getLayoutAttribute(
                        request, PreferencesScope.STRUCTURE, tabId, EXTERNAL_ID_ATTR);
        if (externalId != null) {
            final Map<String, String> allNodesAndValuesForAttribute =
                    stylesheetUserPreferencesService.getAllNodesAndValuesForAttribute(
                            request, PreferencesScope.STRUCTURE, EXTERNAL_ID_ATTR);

            boolean appendNodeId = false;
            for (final Entry<String, String> nodeAttributeEntry :
                    allNodesAndValuesForAttribute.entrySet()) {
                final String entryNodeId = nodeAttributeEntry.getKey();
                final String entryValue = nodeAttributeEntry.getValue();
                if (!tabId.equals(entryNodeId) && externalId.equals(entryValue)) {
                    appendNodeId = true;
                    break;
                }
            }

            if (!FunctionalNameType.isValid(externalId)) {
                logger.warn(
                        "ExternalId {} for tab {} is not a valid fname. It will be converted for use in the URL but this results in additional overhead",
                        externalId,
                        tabId);
                externalId = FunctionalNameType.makeValid(externalId);
            }

            if (appendNodeId) {
                externalId = externalId + PORTLET_PATH_ELEMENT_SEPERATOR + layoutNodeId;
            }

            logger.trace(
                    "Tab identified by {} resolved to externalId {} "
                            + "so returning that externalId as the sole folder name for node.",
                    layoutNodeId,
                    externalId);

            return Arrays.asList(externalId);

        } else {
            logger.trace(
                    "Tab identified by {} had no externalId "
                            + "so returning just {} as the sole folder name for node {}.",
                    layoutNodeId,
                    tabId,
                    layoutNodeId);
            return Arrays.asList(tabId);
        }
    }

    @Override
    public String getLayoutNodeForFolderNames(
            HttpServletRequest request, List<String> folderNames) {
        /*
         * Implementation note: while the API specifies a List of folderNames, this implementation
         * only ever considers the first (presumably, only) value in the list.
         */

        if (folderNames == null || folderNames.isEmpty()) {
            logger.warn(
                    "Asked to get layout node for an empty or null folderNames ({}).", folderNames);
            return null;
        }

        // Check if the folder name is compound and parse it if it is
        String folderName = folderNames.get(0);
        String layoutNodeId = null;
        final int seperatorIndex = folderName.indexOf(PORTLET_PATH_ELEMENT_SEPERATOR);
        if (seperatorIndex > 0 && seperatorIndex < folderName.length() - 1) {
            layoutNodeId = folderName.substring(seperatorIndex + 1);
            folderName = folderName.substring(0, seperatorIndex);
        }

        if (folderNames.size() > 1) {
            logger.warn(
                    "Asked to consider multiple folder names {}, "
                            + "but ignoring all but the first which has been parsed as {}.",
                    folderNames,
                    folderName);
        }

        // Search the users layout attributes for a layout node with a matching externalId value
        String firstMatchingNodeId = null;
        final Map<String, String> allNodesAndValuesForAttribute =
                stylesheetUserPreferencesService.getAllNodesAndValuesForAttribute(
                        request, PreferencesScope.STRUCTURE, EXTERNAL_ID_ATTR);
        for (final Entry<String, String> entry : allNodesAndValuesForAttribute.entrySet()) {
            final String value = entry.getValue();
            // Have to test against the fname safe version of the externalId since the folderName
            // could have already been translated
            if (value.equals(folderName)
                    || FunctionalNameType.makeValid(value).equals(folderName)) {
                final String nodeId = entry.getKey();

                if (nodeId.equals(layoutNodeId)) {
                    // ExternalId matched as well as the layoutNodeId, clear the firstMatchingNodeId
                    // since we found the nodeId here

                    logger.trace("Parsed folder names {} to nodeId {}.", folderNames, nodeId);

                    return nodeId;
                } else if (firstMatchingNodeId == null) {
                    firstMatchingNodeId = nodeId;
                }
            }
        }

        // If an explicit nodeId match isn't found but at least one matching externalId was found
        // use that match
        if (firstMatchingNodeId != null) {
            layoutNodeId = firstMatchingNodeId;
        }
        // In this case the folderName must not have been an externalId, assume it is a layout node
        else if (layoutNodeId == null) {
            layoutNodeId = folderName;
        }

        // Verify the parsed layoutNodeId matches a node in the user's layout
        final IUserInstance userInstance = this.userInstanceManager.getUserInstance(request);
        final IUserPreferencesManager preferencesManager = userInstance.getPreferencesManager();
        final IUserLayoutManager userLayoutManager = preferencesManager.getUserLayoutManager();

        final IUserLayoutNodeDescription node;
        try {
            node = userLayoutManager.getNode(layoutNodeId);
        } catch (PortalException e) {

            logger.warn(
                    "Parsed requested folder names {} to layoutNodeId {} "
                            + "but did not match a node in the user layout.",
                    folderNames,
                    layoutNodeId,
                    e);

            return null;
        }

        if (node == null) {

            logger.warn(
                    "Parsed requested folder names to layoutNodeId {} "
                            + "but did not match a node in the user layout.",
                    folderNames,
                    layoutNodeId);

            return null;
        }

        String nodeId = node.getId();

        logger.trace("Resolved node id {} for folder names {}.", nodeId, folderNames);
        return nodeId;
    }

    @RequestCache(keyMask = {false, true})
    @Override
    public String getFolderNameForPortlet(
            HttpServletRequest request, IPortletWindowId portletWindowId) {
        final IPortletWindow portletWindow =
                this.portletWindowRegistry.getPortletWindow(request, portletWindowId);
        final IPortletEntity portletEntity = portletWindow.getPortletEntity();
        final IPortletDefinition portletDefinition = portletEntity.getPortletDefinition();

        final String fname = portletDefinition.getFName();
        final String layoutNodeId = portletEntity.getLayoutNodeId();

        // Build the targeted portlet string (fname + subscribeId)
        return fname + PORTLET_PATH_ELEMENT_SEPERATOR + layoutNodeId;
    }

    @Override
    public IPortletWindowId getPortletForFolderName(
            HttpServletRequest request, String targetedLayoutNodeId, String folderName) {
        // Basic parsing of the
        final String fname;
        String subscribeId = null;
        final int seperatorIndex = folderName.indexOf(PORTLET_PATH_ELEMENT_SEPERATOR);
        if (seperatorIndex <= 0 || seperatorIndex + 1 == folderName.length()) {
            fname = folderName;
        } else {
            fname = folderName.substring(0, seperatorIndex);
            subscribeId = folderName.substring(seperatorIndex + 1);
        }

        // If a subscribeId was provided validate that it matches up with the fname
        if (subscribeId != null) {
            final IUserInstance userInstance = this.userInstanceManager.getUserInstance(request);
            final IPortletEntity portletEntity =
                    this.portletEntityRegistry.getOrCreatePortletEntity(
                            request, userInstance, subscribeId);
            if (portletEntity == null
                    || !fname.equals(portletEntity.getPortletDefinition().getFName())) {
                // If no entity found or the fname doesn't match ignore the provided subscribeId by
                // setting it to null
                subscribeId = null;
            } else {
                // subscribeId matches fname, lookup the window for the entity and return the
                // windowId
                final IPortletEntityId portletEntityId = portletEntity.getPortletEntityId();
                final IPortletWindow portletWindow =
                        this.portletWindowRegistry.getOrCreateDefaultPortletWindow(
                                request, portletEntityId);
                if (portletWindow == null) {
                    return null;
                }

                return portletWindow.getPortletWindowId();
            }
        }

        // No valid subscribeId, find the best match based on the fname

        // If a layout node is targeted then look for a matching subscribeId under that targeted
        // node
        if (targetedLayoutNodeId != null) {
            final IUserInstance userInstance = this.userInstanceManager.getUserInstance(request);
            final IUserPreferencesManager preferencesManager = userInstance.getPreferencesManager();
            final IUserLayoutManager userLayoutManager = preferencesManager.getUserLayoutManager();

            // First look for the layout node only under the specified folder
            subscribeId = userLayoutManager.getSubscribeId(targetedLayoutNodeId, fname);
        }

        // Find a subscribeId based on the fname
        final IPortletWindow portletWindow;
        if (subscribeId == null) {
            portletWindow =
                    this.portletWindowRegistry.getOrCreateDefaultPortletWindowByFname(
                            request, fname);
        } else {
            portletWindow =
                    this.portletWindowRegistry.getOrCreateDefaultPortletWindowByLayoutNodeId(
                            request, subscribeId);
        }

        if (portletWindow == null) {
            return null;
        }

        return portletWindow.getPortletWindowId();
    }
}
